// Amaze (StateC.java)
// author: Artur Chmiel <artur.chmiel@gmail.com>

package dk.sdu.amaze;

import java.awt.Point;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.geom.RoundRectangle2D.Double;
import java.util.LinkedList;
import java.util.Locale;

import javax.swing.Timer;

import TUIO.TuioCursor;

/*
 * Third game screen: the board
 */
public class StateC extends State implements ActionListener {

	private static final long serialVersionUID = 1L;

	// sounds
	Thread t_start, t_move, t_finalS, t_finalF, t_crush, t_l1;
	Sound start, move, finalS, finalF, crush, l1;

	private Level level;

	TuioCursor activeTuioCursor;

	boolean showingCorrectPath = false;
	boolean isTracking = false;

	int currentStepOnPath = 1;
	Point currentTouchedFieldCoordinates = null;

	boolean showingPlayingInstructions = true;

	boolean isLevelCompleted = false;
	boolean isAvailableTimeElapsed = false;

	Timer timer;
	static public int timerDelay = 10; // timer delay in milliseconds

	long initialTimeMillis = 0; // system time milliseconds
	int elapsedTime = 0; // elapsed time in milliseconds

	public StateC(Amaze context, Level level) {
		super(context);

		timer = new Timer(timerDelay, this);

		// sounds
		finalF = new Sound("time finished.wav"); // time is out
		t_finalF = new Thread(finalF);
		finalS = new Sound("finish1.wav");
		t_finalS = new Thread(finalS); // finish song - winner
		l1 = new Sound("1.wav");
		t_l1 = new Thread(l1); // finish song - winner

		this.level = level;
	}

	public void onFinish() {
		StateB stateB = new StateB(ctx);
		stateB.hitScore(level, elapsedTime);
		ctx.setState(stateB);
	}

	public void onBack() {
		StateB stateB = new StateB(ctx);
		stateB.selectLevel(level);
		ctx.setState(stateB);
	}

	@Override
	public void paint(Graphics g) {
		Graphics2D g2d = (Graphics2D) g;

		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);
		g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
				RenderingHints.VALUE_RENDER_QUALITY);

		AffineTransform initialAffineTransform = g2d.getTransform();

		g2d.setColor(Color.white);
		g2d.fillRect(0, 0, getWidth(), getHeight());

		/* grid */

		int squareSize = 80;

		int columns = level.getColumns();
		int rows = level.getRows();

		GeneralPath gridPath = new GeneralPath();

		for (int i = 1; i < columns; i++) {
			gridPath.moveTo(i * squareSize, 0);
			gridPath.lineTo(i * squareSize, rows * squareSize);
		}

		for (int i = 1; i < rows; i++) {
			gridPath.moveTo(0, i * squareSize);
			gridPath.lineTo(columns * squareSize, i * squareSize);
		}

		/* grid border */

		GeneralPath borderPath = new GeneralPath();

		int right_x = columns * squareSize;
		int bottom_y = rows * squareSize;

		borderPath.moveTo(0, 0);
		borderPath.lineTo(0, bottom_y);
		borderPath.moveTo(right_x, 0);
		borderPath.lineTo(right_x, bottom_y);

		borderPath.moveTo(0, 0);
		borderPath.lineTo(right_x, 0);
		borderPath.moveTo(0, bottom_y);
		borderPath.lineTo(right_x, bottom_y);

		/* game board translation */

		Rectangle2D borderPathBounds = borderPath.getBounds();

		g2d.translate(getWidth() / 2 - borderPathBounds.getWidth() / 2,
				getHeight() / 2 - borderPathBounds.getHeight() / 2);

		/* path background */

		LinkedList<Point> path = level.getPath();

		if (showingCorrectPath || isTracking) {

			g2d.setPaint(Color.orange);

			int steps = (showingCorrectPath) ? path.size()
					: currentStepOnPath - 1;

			for (int i = 0; i < steps; i++)
				g2d.fillRect(path.get(i).x * squareSize, path.get(i).y
						* squareSize, squareSize, squareSize);

			/* current field background */
			g2d.setPaint(Color.orange.darker());

			g2d.fillRect(path.get(currentStepOnPath - 1).x * squareSize, path
					.get(currentStepOnPath - 1).y
					* squareSize, squareSize, squareSize);
		}

		/* board grid */
		g2d.setPaint(Color.lightGray.darker());
		g2d.draw(gridPath);

		/* start and finish point shapes */
		Rectangle2D rectangle = new Rectangle2D.Double(squareSize
				* (path.get(0).x + 0.5) - 8, squareSize * (path.get(0).y + 0.5)
				- 8, 16, 16);

		Ellipse2D.Double circle = new Ellipse2D.Double(squareSize
				* (path.get(path.size() - 1).x + 0.5) - 8, squareSize
				* (path.get(path.size() - 1).y + 0.5) - 8, 16, 16);

		g2d.setPaint(Color.black);
		g2d.setStroke(new BasicStroke(3.0f));
		g2d.draw(rectangle);
		g2d.draw(circle);

		/* path itself */
		if (showingCorrectPath || isTracking) {
			GeneralPath pathShape;

			if (showingCorrectPath)
				pathShape = makePathShape(path, path.size(), squareSize);
			else
				pathShape = makePathShape(path, currentStepOnPath, squareSize);

			g2d.setStroke(new BasicStroke(5.0f));
			g2d.setPaint(Color.white);
			g2d.draw(pathShape);

			g2d.setStroke(new BasicStroke(3.0f));
			g2d.setPaint(Color.black);
			g2d.draw(pathShape);
		}

		/* border */
		g2d.setStroke(new BasicStroke(2.0f));
		g2d.setPaint(Color.black);
		g2d.draw(borderPath);

		/* timer */
		drawTimerOnScreen(g2d, borderPathBounds);

		/* level completed/failed message and playing instructions */
		if (isLevelCompleted || isAvailableTimeElapsed) {

			try {
				if (isLevelCompleted)
					t_finalS.start(); // finish song - winner
				else
					t_finalF.start(); // finish song - time out
			} catch (IllegalThreadStateException e) {
			}

			AffineTransform savedAffineTransform = g2d.getTransform();
			g2d.setTransform(initialAffineTransform);

			g2d.setPaint(new Color(1.0f, 1.0f, 1.0f, .8f));
			g2d.fillRect(0, 0, getWidth(), getHeight());

			String messageString;

			if (isLevelCompleted) {
				String elapsedTimeString = String.format(Locale.US, "%.2f",
						(double) elapsedTime / 1000);

				int lastBestTime = ctx.getBestTime(level.getId());

				if ((lastBestTime != 0) && (elapsedTime < lastBestTime))
					messageString = "Well done, this is a new best time! You have completed the level in "
							+ elapsedTimeString + " seconds!";
				else
					messageString = "Congratulations! You have completed the level in "
							+ elapsedTimeString + " seconds!";
			} else
				messageString = "You are out of time! Try harder next time!";

			drawMessageOnScreen(g2d, messageString, Color.white, Color.red,
					this.getBounds());

			g2d.setTransform(savedAffineTransform);

		} else if (showingPlayingInstructions) {

			drawMessageOnScreen(g2d,
					"Press \u25FB to start or \u25CB to quit.", Color.white,
					new Color(95, 158, 160), borderPathBounds);
		}
	}

	private void drawTimerOnScreen(Graphics2D g2d, Rectangle2D rectangle) {

		String remainingTimeString = String.format(Locale.US, "%.1f",
				(double) (level.getAvailableTime() - elapsedTime) / 1000);

		TextLayout remainingTimeText = new TextLayout("Remaining time: "
				+ remainingTimeString + " sec.", new Font("SansSerif",
				Font.BOLD, 14), new FontRenderContext(null, true, false));

		RoundRectangle2D.Double remainingTimeTextBackground = makeTextLayoutBackground(remainingTimeText);

		Rectangle2D remainingTimeTextBackgroundBounds = remainingTimeTextBackground
				.getBounds();

		AffineTransform savedAffineTransform = g2d.getTransform();

		g2d.translate((rectangle.getWidth() / 2)
				- (remainingTimeTextBackgroundBounds.getWidth() / 2)
				- remainingTimeTextBackgroundBounds.getX(), rectangle
				.getHeight()
				- remainingTimeTextBackgroundBounds.getY() + 12);

		g2d.setPaint(new Color(70, 130, 180)); // steel blue
		g2d.fill(remainingTimeTextBackground);
		g2d.setPaint(Color.white);
		remainingTimeText.draw(g2d, 0, 0);

		g2d.setTransform(savedAffineTransform);
	}

	private void drawMessageOnScreen(Graphics2D g2d, String text,
			Color textColor, Color backgroundColor, Rectangle2D rectangle) {

		TextLayout textLayout = new TextLayout(text, new Font("SansSerif",
				Font.BOLD, 14), new FontRenderContext(null, true, false));

		RoundRectangle2D.Double textLayoutBackground = makeTextLayoutBackground(textLayout);

		Rectangle2D textLayoutBackgroundBounds = textLayoutBackground
				.getBounds();

		AffineTransform af = g2d.getTransform();

		g2d.translate((rectangle.getWidth() / 2)
				- (textLayoutBackgroundBounds.getWidth() / 2)
				- textLayoutBackgroundBounds.getX(),
				(rectangle.getHeight() / 2)
						- (textLayoutBackgroundBounds.getHeight() / 2)
						- textLayoutBackgroundBounds.getY());

		g2d.setPaint(backgroundColor);
		g2d.fill(textLayoutBackground);
		g2d.setPaint(textColor);
		textLayout.draw(g2d, 0, 0);

		g2d.setTransform(af);
	}

	private Double makeTextLayoutBackground(TextLayout text) {
		Rectangle2D textBounds = text.getBounds();

		return new RoundRectangle2D.Double(textBounds.getX() - 7, textBounds
				.getY() - 7, textBounds.getWidth() + 14,
				textBounds.getHeight() + 12, 20, 20);
	}

	private GeneralPath makePathShape(LinkedList<Point> path, int steps,
			int squareSize) {

		GeneralPath gp = new GeneralPath();

		// start point rectangle
		gp.append(new Rectangle2D.Double(
				squareSize * (path.get(0).x + 0.5) - 3, squareSize
						* (path.get(0).y + 0.5) - 3, 6, 6), false);

		for (int i = 1; i < steps; i++) {

			gp.moveTo(squareSize * (path.get(i - 1).x + 0.5), squareSize
					* (path.get(i - 1).y + 0.5));
			gp.lineTo(squareSize * (path.get(i).x + 0.5), squareSize
					* (path.get(i).y + 0.5));
		}

		// finish point circle
		gp.append(new Ellipse2D.Double(squareSize
				* (path.get(steps - 1).x + 0.5) - 3, squareSize
				* (path.get(steps - 1).y + 0.5) - 3, 6, 6), false);

		return gp;
	}

	@Override
	public void addTuioCursor(TuioCursor tcur) {

		if (isLevelCompleted)
			onFinish();
		else if (isAvailableTimeElapsed)
			onBack();

		if (isTracking)
			return;

		Point touchedFieldCoordinates = getFieldCoordinates(tcur);

		// does player want to quit? (yes, this is ugly...)
		if (touchedFieldCoordinates.equals(level.getPath().getLast())) {
			onBack();
		}

		if (touchedFieldCoordinates.equals(level.getPath().getFirst())) {
			activeTuioCursor = tcur;

			showingPlayingInstructions = false;
			showingCorrectPath = true;

			isTracking = true;

			currentStepOnPath = 1;
			currentTouchedFieldCoordinates = touchedFieldCoordinates;

			// sounds
			start = new Sound("start.wav"); // start play(1st button:(0, 5) )
			t_start = new Thread(start);
			t_start.start();

			startTimer();
		}
	}

	@Override
	public void removeTuioCursor(TuioCursor tcur) {

		if (isLevelCompleted || isAvailableTimeElapsed)
			return;

		if ((activeTuioCursor == tcur) && isTracking) {
			stopTimer();

			// sounds
			crush = new Sound("bomb.wav"); // crash the wall
			t_crush = new Thread(crush);
			t_crush.start();

			showingCorrectPath = false;
			isTracking = false;
		}
	}

	@Override
	public void updateTuioCursor(TuioCursor tcur) {

		if (isLevelCompleted || isAvailableTimeElapsed)
			return;

		if (activeTuioCursor == tcur) {

			Point touchedFieldCoordinates = getFieldCoordinates(tcur);

			if (isTracking
					&& !(touchedFieldCoordinates
							.equals(currentTouchedFieldCoordinates))) {

				Point pathFieldCoordinates = level.getPath().get(
						currentStepOnPath);

				showingCorrectPath = false;

				if (touchedFieldCoordinates.equals(pathFieldCoordinates)) {
					currentStepOnPath++;
					currentTouchedFieldCoordinates = touchedFieldCoordinates;

					// sounds
					move = new Sound("move.wav");
					t_move = new Thread(move);
					t_move.start();

					if (currentStepOnPath == level.getPath().size()) {
						stopTimer();
						isLevelCompleted = true;
					}

				} else {
					stopTimer();
					isTracking = false;

					// sounds
					crush = new Sound("bomb.wav"); // crash the wall
					t_crush = new Thread(crush);
					t_crush.start();
				}
			}
		}
	}

	private Point getFieldCoordinates(TuioCursor tcur) {
		return new Point((int) (tcur.getX() * level.getColumns()), (int) (tcur
				.getY() * level.getRows()));
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		// increase elapsed time
		long dt = System.currentTimeMillis() - initialTimeMillis;
		elapsedTime += dt;
		initialTimeMillis = System.currentTimeMillis();

		int availableTime = level.getAvailableTime();

		if (elapsedTime >= availableTime) {
			stopTimer();
			elapsedTime = availableTime; // for proper display
			isAvailableTimeElapsed = true;
		}

		// repaint game board
		this.repaint();
	}

	private void startTimer() {
		initialTimeMillis = System.currentTimeMillis();
		timer.start();
	}

	private void stopTimer() {
		timer.stop();
	}
}
